DASCore - Patch

2025-04-14

Patch Basics

This notebook shows how to work with DASCore’s Patch.
It is a shortened version of the DASCore’s Patch tutorial.

DASCore Installation Check


%%capture #Suppressing output

try:
    import dascore as dc
except ImportError:
    !pip install dascore
    !pip install ipympl
    # restart kernel
    import IPython
    IPython.Application.instance().kernel.do_shutdown(True) # automatically restarts kernel

DASCore: Data structures


Patch

Spool

DASCore: Patch


  • Patch is composed of:
    • coords: the coordinates of each dimension (e.g. coord range, step, units)
    • data: the array of measurements
    • attrs: the non-coordinate metadata (e.g. cable id)
    • dims: a tuple of dimension names (e.g. distance, frequency)
  • The Patch is immutable which means that once a patch is created it cannot (easily) be changed.

DASCore: Patch


  • The get_example_patch function is useful for loading example/test patches.
patch = dc.get_example_patch("example_event_1")
  • Display the string representation of the patch object:
print(patch)

DASCore: Patch


DASCore Patch ⚡
---------------
➤ Coordinates (distance: 601, time: 1001)
    *distance: CoordRange( min: 400 max: 1000 step: 1 shape: (601,) dtype: int64 )
    *time: CoordRange( min: 00:00:00.119999999 max: 00:00:00.219999999 step: 0.0001s shape: (1001,) dtype: 
datetime64[ns] units: s )
➤ Data (float64)
   [[  -81.   -68.   391. ...    -5.   151.   -22.]
    [ -498.   514.     3. ...    -4.   100.  -149.]
    [ -262.   486.   -79. ...   -47.    28.  -320.]
    ...
    [  333.  1057.  -641. ...   693.   163. -1426.]
    [  405.  -673.   150. ...   209.   232.  -354.]
    [  226.  -524.   378. ...    -5.  -105.  -208.]]
➤ Attributes
    cable_id: 

Patch components: Data and Dims


  • The data array can be accessed with data and updated with Patch.update.
  • Get the data array
array = patch.data
  • Divide the data by 1_000_000 and create a new patch.
patch_new_data = patch.update(data=array / 1_000_000)
  • The dimensions are accessed via the dims attribute.
print(patch.dims)
('distance', 'time')

Exercise (Patch data)


  • Calculate and print the following:
  1. The number of samples in the data
num_samples = patch.data.size
print(f"Number of samples: {num_samples}")
Number of samples: 601601
  1. The minimum and maximum value of the data
min_val = patch.data.min()
max_val = patch.data.max()
print(f"Minimum value: {min_val}")
print(f"Maximum value: {max_val}")
Minimum value: -6102.0
Maximum value: 5975.00146484375

Patch components: Attrs


  • Attrs contain non-dimensional metdata: acquisition/interrogator identifiers, custom tags, etc.
print(patch.attrs)
PatchAttrs(
    data_type='',
    data_category='',
    data_units=None,
    instrument_id='',
    acquisition_id='',
    tag='',
    station='',
    network='',
    history=(),
    dims='distance,time',
    coords=<FrozenDict {'distance': CoordSummary(dtype='int64', min=400, max=1000, step=1, units=None), 'time': 
CoordSummary(dtype='datetime64', min=numpy.datetime64('1970-01-01T00:00:00.119999999'), 
max=numpy.datetime64('1970-01-01T00:00:00.219999999'), step=numpy.timedelta64(100000,'ns'), units=<Quantity(1, 
'second')>)}>,
    cable_id=''
)

Patch components: Attrs


  • Set or update Attrs
patch_updated_attrs = patch.update_attrs(
    acquisition_id="experiment_12",
    cable_id="b202393ad",
)
print(patch_updated_attrs)
DASCore Patch ⚡
---------------
➤ Coordinates (distance: 601, time: 1001)
    *distance: CoordRange( min: 400 max: 1000 step: 1 shape: (601,) dtype: int64 )
    *time: CoordRange( min: 00:00:00.119999999 max: 00:00:00.219999999 step: 0.0001s shape: (1001,) dtype: 
datetime64[ns] units: s )
➤ Data (float64)
   [[  -81.   -68.   391. ...    -5.   151.   -22.]
    [ -498.   514.     3. ...    -4.   100.  -149.]
    [ -262.   486.   -79. ...   -47.    28.  -320.]
    ...
    [  333.  1057.  -641. ...   693.   163. -1426.]
    [  405.  -673.   150. ...   209.   232.  -354.]
    [  226.  -524.   378. ...    -5.  -105.  -208.]]
➤ Attributes
    acquisition_id: experiment_12
    cable_id: b202393ad

Exercise (Patch attrs)


  • Do the following:
  1. Print the cable_id from the patch with updated attrs.
  2. Create a new patch with a station_name of “DAS1”

Exercise (Patch attrs) - Solution


  • Do the following:
  1. Print the cable_id from the patch with updated attrs.
print(f"Cable ID: {patch_updated_attrs.attrs['cable_id']}")
Cable ID: b202393ad

Exercise (Patch attrs) - Solution


  • Do the following:
  1. Create a new patch with a station_name of “DAS1”.
patch_with_station = patch.update_attrs(station_name="DAS1")
print(patch_with_station.attrs)
PatchAttrs(
    data_type='',
    data_category='',
    data_units=None,
    instrument_id='',
    acquisition_id='',
    tag='',
    station='',
    network='',
    history=(),
    dims='distance,time',
    coords=<FrozenDict {'distance': CoordSummary(dtype='int64', min=400, max=1000, step=1, units=None), 'time': 
CoordSummary(dtype='datetime64', min=numpy.datetime64('1970-01-01T00:00:00.119999999'), 
max=numpy.datetime64('1970-01-01T00:00:00.219999999'), step=numpy.timedelta64(100000,'ns'), units=<Quantity(1, 
'second')>)}>,
    cable_id='',
    station_name='DAS1'
)

Patch components: Coords


  • Coords represent information about the coordinates (not coordinate systems) associated with a patch. These include, but aren’t limited to, the dimensions such as ‘time’ and ‘distance’.
  • The coordinates and their labels.
print(patch.coords)
Coordinates (distance: 601, time: 1001)
    *distance: CoordRange( min: 400 max: 1000 step: 1 shape: (601,) dtype: int64 )
    *time: CoordRange( min: 00:00:00.119999999 max: 00:00:00.219999999 step: 0.0001s shape: (1001,) dtype: 
datetime64[ns] units: s )

Patch components: Coords


  • Coords objects can be accessed with Patch.get_coord
dist_coord = patch.get_coord("distance")
  • Get the start, stop, step, and units of the distance coord.
dist_start = dist_coord.min()
dist_stop = dist_coord.max()
dist_step = dist_coord.step
dist_units = dist_coord.units
  • Get the underlying array data in the coordinate.
dist_array = dist_coord.values

Patch components: Coords


  • Alternatively, Patch.get_array simply returns the numpy array of the coordinate.
  • The values of the distance dimension
dist = patch.get_array("distance")
  • The value of the time dimension using a numpy datetime64 array.
time = patch.get_array("time")
  • Convert the array to seconds from 1970 (floats).
time_s = dc.to_float(time)

Patch components: Coords


  • Coordinates can be renamed using Patch.rename_coord.
patch_renamed_coords = patch.rename_coords(distance="depth")
print(patch_renamed_coords.dims)
('depth', 'time')
  • Or updated using Patch.update_coords
from dascore.units import m

# Add 12 to the current distance values.
dist = patch.get_array("distance")
patch_new_dist = patch.update_coords(distance=(dist + 12)*m)

dist2 = patch_new_dist.get_coord("distance")
print(dist2)
CoordRange( min: 412 max: 1012 step: 1 shape: (601,) dtype: int64 units: m )

Exercise (Patch coords)


  • Calculate the and print following parameters:
  1. The duration (time) of the patch recording using the time coordinate.
  2. Reset the start of the time coordiante (e.g., 08:00, April 14, 2025).

Exercise (Patch coords)


  • Calculate the and print following parameters:
  1. The duration (time) of the patch recording using the time coordinate.
time_coord = patch.get_coord("time")
time_start = time_coord.min()
time_end = time_coord.max()
duration = time_end - time_start
print(f"Duration: {duration}")
Duration: 100000000 nanoseconds

Exercise (Patch coords)


  • Calculate the and print following parameters:
  1. Reset the start of the time coordiante (e.g., 08:00, April 14, 2025).
# New start time
new_start = np.datetime64('2025-04-14T08:00:00')
current_time = patch.get_array("time")

# Calculate time differences from start
time_delta = current_time - current_time[0] 

# Add to new start time
new_time = new_start + time_delta  

# Create new patch with updated time coordinate
patch_new = patch.update_coords(time=new_time)

print(f"New time range: {patch_new.get_coord('time').min()}\
        to {patch_new.get_coord('time').max()}")
New time range: 2025-04-14T08:00:00.000000000 to 2025-04-14T08:00:00.100000000

Visualization


  • DASCore provides visualization in the patch.viz namespace.
  • The classic waterfall plot
patch.viz.waterfall()

  • Adjust the scale parameter
patch.viz.waterfall(scale=0.05)

Visualization


  • DASCore provides visualization in the patch.viz namespace.
  • The wiggle plot
patch.viz.wiggle()

  • Adjust the scale parameter
patch.viz.wiggle(scale=0.1)

Trimming and sub-selection


  • Patch.selectis used to trim patches. For example, to zoom in on the down-going reflection in our example patch.
trimmed = patch.select(time=(.16, .22), distance =(600,800))
trimmed.viz.waterfall(scale=0.1)

Trimming and sub-selection


  • Trimming can also be done via samples
  • Remove 20 samples from start and end of time dimension using samples=True
trimmed_patch = patch.select(time=(20, -20), samples=True)
trimmed.viz.waterfall(scale=0.05)

  • Remove 0.02 seconds from start and 0.03 seconds from the end of patch using relative=True
trimmed_patch = patch.select(time=(0.02, -0.03), relative=True)
trimmed.viz.waterfall(scale=0.05)

Exercise (Patch select)


  • Remove the first 10 spatial channels then the last 0.05 seconds.
# First, remove the first 10 spatial channels
patch_trimmed_distance = patch.select(distance=(10, None), samples=True)

# Then, remove the last 0.05 seconds
patch_final_trim = patch_trimmed_distance.select(time=(None, -0.05), relative=True)

# Verify the trimming worked
print(f"Original patch shape: {patch.data.shape}")
print(f"Trimmed patch shape: {patch_final_trim.data.shape}")
Original patch shape: (601, 1001)
Trimmed patch shape: (591, 501)

Patch processing


  • DASCore provides many processing patch methods. This section will highlight a few of these.
  • Pass filtering
    The Patch.pass_filter method is used to apply bandpass, lowpass, and highpass SOS filters to the data along a specified dimension.
# apply a 100Hz to 300Hz highpass
patch_bp = patch.pass_filter(time=(100, 300))  

# apply a 300Hz lowpass
patch_lp = patch.pass_filter(time=(..., 300))  

# apply a 50Hz highpass
patch_hp = patch.pass_filter(time=(50, ...))   

Patch processing


  • Time units
from dascore.units import Hz, s

patch = dc.get_example_patch()

# Band-pass filter between 10Hz and 40Hz
patch_filt = patch.pass_filter(time=(10*Hz, 40*Hz))

# Band-pass filter between 20s and 2s
patch_filt = patch.pass_filter(time=(2*s, 20*s))

Patch processing


  • Frequency-wavenumber filter
import dascore as dc
import matplotlib.pyplot as plt

patch = dc.get_example_patch("example_event_1")

patch_proc = (
    patch.set_units("1/s", distance="m", time="s")
    .detrend("time")
    .taper(time=0.05)
    .slope_filter(filt=[2e3,2.2e3,8e3,2e4],
                directional=True,
                notch=False)
)

Patch processing


  • Frequency-wavenumber filter

Exercise (Patch processing 1)


  • Plot each of the filtered patches above. Which filtering technique did the best at accentuating the event signal?
import matplotlib.pyplot as plt

# apply a 100Hz to 300Hz highpass
patch_bp = patch.pass_filter(time=(100, 300))

ax = plt.subplot()
patch_bp.viz.waterfall(ax=ax, scale=0.1)
ax.set_title('Bandpass Filter (100-300 Hz)',fontsize=16)
plt.show()

Exercise (Patch processing 1)


  • Plot each of the filtered patches above. Which filtering technique did the best at accentuating the event signal?

Patch processing


  • Decimate:
    Patch.decimate reduces the number of samples in the specified dimension. By default, a lowpass filter is applied to avoid aliasing.
# keep every 10th sample along time axis
patch_dec = patch.decimate(time=10)
  • Detrend Patch.detrend applies a linear detrend along a specified dimension (axis).
patch_detrended = patch.detrend("time")
  • Resample Like Patch.decimate, Patch.resample is used to change the sampling rate of the patch along a specific dimension. Unlike Patch.decimate, however, non-integar multiples can be used.
# change spatial sampling to 15m
patch_resamp = patch.resample(distance=15)  

Exercise (Patch processing 2)


  • Use aggregate and mean to calculate the same patch.
mean_1 = patch.aggregate("time", method="mean")
mean_2 = patch.mean("time")

Exercise (Patch processing 2)


  • Aggregate supports any tensor reduction operation.
reduced = patch.aggregate("time", np.sum)
print(reduced)
DASCore Patch ⚡
---------------
➤ Coordinates (distance: 601, time: 1)
    *distance: CoordRange( min: 400 max: 1000 step: 1 shape: (601,) dtype: int64 )
    *time: CoordPartial( step: 0.0001s shape: (1,) dtype: datetime64[ns] units: s )
➤ Data (float64)
   [[-942.001]
    [-973.998]
    [-967.   ]
    ...
    [-888.998]
    [ -93.   ]
    [-426.001]]
➤ Attributes
    history: ("aggregate(dim='time',dim_reduce='empty',method='<function sum at 0x107f35d00>')",)
    cable_id: 

Exercise (Patch processing 2)


  1. Try the normalize method to normalize trace amplitudes
patch_norm = patch.normalize("time")


2. Try the taper method to apply a taper to the edges

# Apply an Hanning taper to 5% of each end for time dimension.
patch_taper1 = patch.taper(time=0.05, window_type="hann")

# Apply a triangular taper to 10% of the start of the distance dimension.
patch_taper2 = patch.taper(distance=(0.10, None), window_type='triang')


3. Try the differentiate method (velocity to acceleration)

patch_diff = patch.differentiate("time")

Patch transformation


  • In addition to the processing methods, DASCore includes several patch transformation methods. This section shows some examples.
  1. Load a cleaned-up patch from the previous example.
import dascore as dc
patch = dc.get_example_patch("example_event_2")


  1. Perform an real Discrete Fourier Transform (DFT) along the time axis.
patch_fft = patch.dft("time", real=True)


  1. Integrate along time domain
patch_int = patch.integrate("time")

DASCore: Patch

  • Processing methods are chained together
import dascore as dc
patch = dc.get_example_patch("example_event_1")

patch_proc = (
    patch.set_units("1/s", distance="m", time="s")
    .detrend("time")
    .taper(time=0.05)
    .pass_filter(time=(..., 300))
)

patch_proc.viz.waterfall(show=True, scale=0.35)

DASCore: Patch → ObsPy


import dascore as dc
patch = dc.get_example_patch()

stream = patch.io.to_obspy()